<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="xWfL9" id="xWfL9"><span data-lake-id="ue105be8b" id="ue105be8b">典型回答</span></h1>
  <p data-lake-id="uf07a5adc" id="uf07a5adc"><span data-lake-id="u3244b38e" id="u3244b38e">要想理解零拷贝，首先要了解操作系统的IO流程，因为有内核态和用户态的区别，为了保证安全性和缓存，普通的读写流程如下：</span></p>
  <p data-lake-id="u8717881c" id="u8717881c"><span data-lake-id="u8dbdd3f7" id="u8dbdd3f7">（对于Java程序，还会多了一个堆外内存和堆内存之间的copy）</span></p>
  <p data-lake-id="u47a51a1c" id="u47a51a1c"><img src="https://cdn.nlark.com/yuque/0/2023/png/719664/1675513451422-d76e4b33-486e-4d86-9720-330f205e6338.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_36%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u13fa2935" id="u13fa2935"><span data-lake-id="ub8545157" id="ub8545157">整体的流程如下所示：</span></p>
  <ol list="uba45187d">
   <li fid="u38e60674" data-lake-id="u236f9f69" id="u236f9f69"><span data-lake-id="u609056cf" id="u609056cf">用户read发起系统调用，由用户态进入内核态，通过DMA技术</span><strong><span data-lake-id="u8f9bde5c" id="u8f9bde5c">将磁盘中的数据copy到内核缓冲区中</span></strong></li>
   <li fid="u38e60674" data-lake-id="u5a1d2177" id="u5a1d2177"><span data-lake-id="ub4e803ad" id="ub4e803ad">当DMA完成工作后，会发起一个中断通知CPU数据拷贝完成，然后CPU再</span><strong><span data-lake-id="ue50bd51c" id="ue50bd51c">将内核态中的数据copy到用户态中</span></strong></li>
   <li fid="u38e60674" data-lake-id="u484e408a" id="u484e408a"><span data-lake-id="u0413f40a" id="u0413f40a">内核唤醒对应线程，同时将用户态的数据返回给该线程空间</span></li>
   <li fid="u38e60674" data-lake-id="u568ef294" id="u568ef294"><span data-lake-id="uacc833a8" id="uacc833a8">用户态线程进行业务处理</span></li>
   <li fid="u38e60674" data-lake-id="u534e1016" id="u534e1016"><span data-lake-id="u004f3a26" id="u004f3a26">当服务器对请求进行响应的时候，会发起系统调用，由内核</span><strong><span data-lake-id="u339069fb" id="u339069fb">将用户态的数据copy到内核态中</span></strong></li>
   <li fid="u38e60674" data-lake-id="ud629eea2" id="ud629eea2"><span data-lake-id="u9c587668" id="u9c587668">复制完毕后，再有网络适配器通过DMA技术</span><strong><span data-lake-id="u4164704b" id="u4164704b">将内核态缓冲区中的数据copy到网卡中</span></strong><span data-lake-id="u4f97000c" id="u4f97000c">，完成后，内核态会返回到用户态</span></li>
   <li fid="u38e60674" data-lake-id="ud3bdc7a5" id="ud3bdc7a5"><span data-lake-id="u30bb44d3" id="u30bb44d3">最后由网卡将数据发送出去</span></li>
  </ol>
  <p data-lake-id="ue9adc9fc" id="ue9adc9fc"><span data-lake-id="u1682aa9d" id="u1682aa9d">在这个过程中，如果不考虑用户态的内存拷贝和物理设备到驱动的数据拷贝，我们会发现，这其中会涉及4次数据拷贝。同时也会涉及到4次进程上下文的切换。</span><strong><span data-lake-id="u2fc4a40f" id="u2fc4a40f">所谓的零拷贝，作用就是通过各种方式，在特殊情况下，减少数据拷贝的次数/减少CPU参与数据拷贝的次数。</span></strong></p>
  <p data-lake-id="u3e92b8a8" id="u3e92b8a8"><span data-lake-id="u3d2405d9" id="u3d2405d9">常见的零拷贝方式有mmap，sendfile，dma，directI/O等。</span></p>
  <h1 data-lake-id="VDHV9" id="VDHV9"><span data-lake-id="u118a5087" id="u118a5087">扩展知识</span></h1>
  <h2 data-lake-id="hDfbk" id="hDfbk"><span data-lake-id="u1fedfa58" id="u1fedfa58">DMA</span></h2>
  <p data-lake-id="u2519cf59" id="u2519cf59"><span data-lake-id="u7a202821" id="u7a202821">正常的IO流程中，不管是物理设备之间的数据拷贝，如磁盘到内存，还是内存之间的数据拷贝，如用户态到内核态，都是需要CPU参与的，如下所示</span></p>
  <p data-lake-id="u3899d672" id="u3899d672"><img src="https://cdn.nlark.com/yuque/0/2023/png/719664/1675514101817-0f1dc1ff-aa39-4057-8fb2-d53d10309646.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_19%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"><span data-lake-id="u6f175777" id="u6f175777"><br></span><span data-lake-id="uf8ce6e6a" id="uf8ce6e6a">如果是比较大的文件，这样无意义的copy显然会极大的浪费CPU的效率，所以就诞生了DMA</span></p>
  <p data-lake-id="u53e0b653" id="u53e0b653"><span data-lake-id="ucad7458d" id="ucad7458d">DMA的全称是Direct Memory Access，顾名思义，DMA的作用就是直接将IO设备的数据拷贝到内核缓冲区中。使用DMA的好处就是IO设备到内核之间的数据拷贝不需要CPU的参与，CPU只需要给DMA发送copy指令即可，提高了处理器的利用效率，如下所示：</span></p>
  <p data-lake-id="u7b0ecec7" id="u7b0ecec7"><img src="https://cdn.nlark.com/yuque/0/2023/png/719664/1675514204952-48791660-5d6a-424c-99bb-ccf041423d7d.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_18%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <h2 data-lake-id="C3xyO" id="C3xyO"><span data-lake-id="ua6c2921c" id="ua6c2921c">mmap</span></h2>
  <p data-lake-id="ud91f8710" id="ud91f8710"><span data-lake-id="u0fbc8afc" id="u0fbc8afc">上文我们说到，正常的read+write，都会经历至少四次数据拷贝的，其中就包括内核态到用户态的拷贝，它的作用是为了安全和缓存。如果我们能保证安全性，是否就让用户态和内核态共享一个缓冲区呢？这就是mmap的作用。</span></p>
  <p data-lake-id="ud4b43922" id="ud4b43922"><span data-lake-id="u09475ee1" id="u09475ee1">​</span><br></p>
  <p data-lake-id="u442b772a" id="u442b772a"><span data-lake-id="uf388efcd" id="uf388efcd">mmap，全称是memory map，翻译过来就是内存映射，顾名思义，就是将内核态和用户态的内存映射到一起，避免来回拷贝，实现这样的映射关系后，进程就可以采用指针的方式读写操作这一段内存，而系统会自动回写脏页面到对应的文件磁盘上，即完成了对文件的操作而不必再调用 read、write 等系统调用函数。相反，内核空间对这段区域的修改也直接反映用户空间，从而可以实现不同进程间的文件共享。其函数签名如下：</span></p>
  <pre lang="java"><code>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
</code></pre>
  <p data-lake-id="ua5ef97fb" id="ua5ef97fb"><span data-lake-id="ucceafbbf" id="ucceafbbf">一般来讲，mmap会代替read方法，模型如下图所示：</span></p>
  <p data-lake-id="ua0fd84a5" id="ua0fd84a5"><img src="https://cdn.nlark.com/yuque/0/2023/png/719664/1675516415369-306c6a6d-da79-48ce-a15e-7618c0882b2e.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_35%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"><span data-lake-id="ud131d63e" id="ud131d63e"><br></span><span data-lake-id="ud29084a7" id="ud29084a7">如果这个时候系统进行IO的话，采用mmap + write的方式，内存拷贝的次数会变为3次，上下文切换则依旧是4次。</span></p>
  <p data-lake-id="u5a07fff1" id="u5a07fff1"><span data-lake-id="u8f830d86" id="u8f830d86">​</span><br></p>
  <p data-lake-id="u040baff5" id="u040baff5"><span data-lake-id="ua6c1f2c9" id="ua6c1f2c9">需要注意的是，mmap采用基于缺页异常的懒加载模式。通过 mmap 申请 1000G 内存可能仅仅占用了 100MB 的虚拟内存空间，甚至没有分配实际的物理内存空间，只有当真正访问的时候，才会通过缺页中断的方式分配内存</span></p>
  <p data-lake-id="uc21e81fb" id="uc21e81fb"><strong><span data-lake-id="uaedec264" id="uaedec264">但是mmap不是银弹</span></strong><span data-lake-id="u1985c3ad" id="u1985c3ad">，有如下原因：</span></p>
  <p data-lake-id="u41766953" id="u41766953"><span data-lake-id="u9e92c652" id="u9e92c652">​</span><br></p>
  <ol list="uf562fcc3">
   <li fid="u69f2f950" data-lake-id="u83305c19" id="u83305c19"><span data-lake-id="uaf055e2f" id="uaf055e2f">mmap 使用时必须实现指定好内存映射的大小，因此 mmap 并不适合变长文件；</span></li>
   <li fid="u69f2f950" data-lake-id="uda4880d2" id="uda4880d2"><span data-lake-id="ueabe725a" id="ueabe725a">因为mmap在文件更新后会通过OS自动将脏页回写到disk中，所以在随机写很多的情况下，mmap 方式在效率上不一定会比带缓冲区的一般写快；</span></li>
   <li fid="u69f2f950" data-lake-id="ua9df39d9" id="ua9df39d9"><span data-lake-id="u92a8c1c5" id="u92a8c1c5">因为mmap必须要在内存中找到一块连续的地址块，如果在 32-bits 的操作系统上，虚拟内存总大小也就 2GB左右（32位系统的地址空间最大为4G，除去1G系统，用户能使用的内存最多为3G左右（windows内核较大，一般用户只剩下2G可用）。），此时就很难对 4GB 大小的文件完全进行 mmap，所以对于超大文件来讲，mmap并不适合</span></li>
  </ol>
  <h2 data-lake-id="kgpE8" id="kgpE8"><span data-lake-id="uf8665a46" id="uf8665a46">sendfile</span></h2>
  <p data-lake-id="ub5f72ec7" id="ub5f72ec7"><span data-lake-id="uae514fe4" id="uae514fe4">如果只是传输数据，并不对数据作任何处理，譬如将服务器存储的静态文件，如html，js发送到客户端用于浏览器渲染，在这种场景下，如果依然进行这么多数据拷贝和上下文切换，简直就是丧心病狂有木有！所以我们就可以通过sendfile的方式，只做文件传输，而不通过用户态进行干预：</span></p>
  <p data-lake-id="u7518ded7" id="u7518ded7"><img src="https://cdn.nlark.com/yuque/0/2023/png/719664/1675517973893-8ec1067b-0635-4803-8922-83251e13f301.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_34%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="ub9fe6215" id="ub9fe6215"><span data-lake-id="u92655ad4" id="u92655ad4">此时我们发现，数据拷贝变成了3次，上下文切换减少到了2次。</span></p>
  <p data-lake-id="u09a7d473" id="u09a7d473"><span data-lake-id="u04976607" id="u04976607">虽然这个时候已经优化了不少，但是我们还有一个问题，为什么内核要拷贝两次（page cache -&gt; socket cache），能不能省略这个步骤？当然可以</span></p>
  <h3 data-lake-id="yHJXB" id="yHJXB"><span data-lake-id="ub8f77579" id="ub8f77579">sendfile + DMA Scatter/Gather</span></h3>
  <p data-lake-id="u14494890" id="u14494890"><span data-lake-id="u5bdb02bf" id="u5bdb02bf">DMA gather是LInux2.4新引入的功能，它可以读page cache中的数据描述信息（内存地址和偏移量）记录到socket cache中，由 DMA 根据这些将数据从读缓冲区拷贝到网卡，相比之前版本减少了一次CPU拷贝的过程，如下图所示：</span></p>
  <p data-lake-id="ub94d94e1" id="ub94d94e1"><img src="https://cdn.nlark.com/yuque/0/2023/png/719664/1675518151888-2588819c-5e8f-4e90-a359-fb7461cbf036.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_33%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <h2 data-lake-id="hTZYL" id="hTZYL"><span data-lake-id="u00faf8a6" id="u00faf8a6">direct I/O</span></h2>
  <p data-lake-id="u94c72738" id="u94c72738"><span data-lake-id="u265b1df3" id="u265b1df3">之前的mmap可以让用户态和内核态共用一个内存空间来减少拷贝，其实还有一个方式，就是硬件数据不经过内核态的空间，直接到用户态的内存中，这种方式就是Direct I/O。换句话说，Direct I/O不会经过内核态，而是用户态和设备的直接交互，用户态的写入就是直接写入到磁盘，不会再经过操作系统刷盘处理。</span></p>
  <p data-lake-id="u73d10d0c" id="u73d10d0c"><span data-lake-id="u6581d84c" id="u6581d84c">这样确实拷贝次数减少，读取速度会变快，但是因为操作系统不再负责缓存之类的管理，这就必须交由应用程序自己去做，譬如MySql就是自己通过Direct I/O完成的，同时MySql也有一套自己的缓存系统</span></p>
  <p data-lake-id="ub50acfac" id="ub50acfac"><span data-lake-id="ub6eafcc9" id="ub6eafcc9">同时，虽然direct I/O可以直接将文件写入磁盘中，但是文件相关的元信息还是要通过fsync缓存到内核空间中</span></p>
  <p data-lake-id="u1fca989a" id="u1fca989a"><span data-lake-id="ubfc05296" id="ubfc05296">​</span><br></p>
 </body>
</html>